package cn.geminis.lowcode.compiler.code;

import cn.geminis.lowcode.compiler.Context;
import cn.geminis.lowcode.compiler.io.TreeFileObject;
import cn.geminis.lowcode.compiler.util.JdkCompilerClass;
import cn.geminis.lowcode.compiler.util.JdkTreeMakerUtils;
import cn.geminis.lowcode.utils.Utils;
import com.sun.source.tree.ClassTree;
import lombok.Cleanup;
import lombok.NonNull;
import lombok.SneakyThrows;

import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import java.util.List;

/**
 * @author puddi
 */
public class LcClass extends AbstractCode<ClassTree, TypeElement> {
    private LcPackage pkg;
    private LcModifier modifiers;
    private String name;

    public LcClass(Element element) {
        super(element);
    }

    public LcClass(@NonNull LcPackage pkg, @NonNull LcModifier modifiers, @NonNull String name) {
        this.pkg = pkg;
        this.modifiers = modifiers;
        this.name = name;
    }

    @Override
    protected ClassTree createJdkTree() {
        return JdkTreeMakerUtils.classDef(this.modifiers.getJdkTree(), this.name);
    }

    @Override
    protected TypeElement createJdkElement() {
        var classTree = this.getJdkTree();
        var compilationUnit = createCompilationUnit();
        var enter = Context.INSTANCE.getEnter();
        var compilationUnitEnv = Utils.callMethod(enter, "topLevelEnv", compilationUnit);

        var compiler = Context.INSTANCE.getJavaCompiler();
        Utils.callMethod(compiler, "enterTrees", Utils.listFrom(List.of(compilationUnit)));

        var classEnv = Utils.callMethod(enter, "classEnv", classTree, compilationUnitEnv);
        Utils.callMethod(compiler, "attribute", classEnv);

        classTree = Utils.callMethodWithTypes(
                Context.INSTANCE.getLambdaToMethod(),
                "translateTopLevelClass",
                new Class[]{
                        JdkCompilerClass.ENV,
                        JdkCompilerClass.JC_TREE,
                        JdkCompilerClass.TREE_MAKER
                },
                classEnv,
                classTree,
                Context.INSTANCE.getTreeMaker()
        );

        Utils.callMethod(Context.INSTANCE.getGen(), "genClass", compilationUnitEnv, classTree);

        var element = (TypeElement) Utils.getField(classTree, "sym");
        Utils.setField(element, "sourcefile", null);
        return element;
    }

    public void generateClassFile() {
        var element = this.getJdkElement();
        Utils.callMethodWithTypes(
                Context.INSTANCE.getClassWriter(),
                "writeClass",
                new Class[]{
                        JdkCompilerClass.CLASS_SYMBOL
                },
                element
        );
    }

    @SneakyThrows
    public void generateSourceFile() {
        var file = Context.INSTANCE.getProcessingEnv().getFiler().createSourceFile(this.pkg.getJdkTree().getPackageName().toString() + "." + this.name);
        @Cleanup var writer = file.openWriter();
        writer.write(this.createCompilationUnit().toString());
    }

    public void append(AbstractCode<?, ?> newElement) {
        var jdkTree = this.getJdkTree();
        var defs = Utils.getField(jdkTree, "defs");
        var newDefs = Utils.callMethodWithTypes(
                defs,
                "append",
                new Class[]{
                        Object.class
                },
                newElement.getJdkTree());
        Utils.setField(jdkTree, "defs", newDefs);
        super.treeChanged();
    }

    private Object createCompilationUnit() {
        var classTree = this.getJdkTree();
        var treeMaker = Context.INSTANCE.getTreeMaker();
        var compilationUnit = Utils.callMethod(
                treeMaker,
                "TopLevel",
                Utils.listFrom(List.of(
                        this.pkg.getJdkTree(),
                        classTree
                ))
        );
        Utils.setField(compilationUnit, "sourcefile", new TreeFileObject(classTree));
        var symtab = Context.INSTANCE.getSymtab();
        Utils.setField(compilationUnit, "packge", pkg.getJdkElement());
        Utils.setField(compilationUnit, "modle", Utils.getField(symtab, "unnamedModule"));
        return compilationUnit;
    }
}
